# Copyright (c) 2025 Anfer / Ancante
# 基于 unlock-music 项目 (c) 2019-2023 MengYX 的解密算法实现

"""
=============================================================================
开源声明

本脚本中的 NCM 解密算法是基于 unlock-music 项目的解密算法实现进行 Python 重写的。
unlock-music 是一个在 MIT 许可下开源的项目，用于在浏览器中解锁加密的音乐文件。

特别感谢 unlock-music 项目的贡献者们提供的技术实现参考：
项目地址：https://git.unlock-music.dev/um/web
许可证：MIT License (https://git.unlock-music.dev/um/web/src/branch/master/LICENSE)

本项目基于 MIT 许可进行分发，与原项目采用相同的许可证。
=============================================================================
"""

import binascii
import struct
import base64
import json
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

# 核心密钥和元数据密钥
CORE_KEY = binascii.a2b_hex("687A4852416D736F356B496E62617857")
META_KEY = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")

# 音频格式标识
FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43]  # fLaC
MP3_HEADER = [0x49, 0x44, 0x33]  # ID3

def sniff_audio_format(data):
    """根据音频数据前几个字节判断音频格式"""
    if len(data) >= 4 and list(data[:4]) == FLAC_HEADER:
        return 'flac'
    elif len(data) >= 3 and list(data[:3]) == MP3_HEADER:
        return 'mp3'
    else:
        return 'mp3'  # 默认返回mp3

def decrypt_ncm(file_path):
    """
    解密NCM文件
    
    Args:
        file_path (str): NCM文件路径
        
    Returns:
        tuple: (音频数据, 元数据字典)
    """
    # 读取文件内容
    with open(file_path, 'rb') as f:
        # 读取整个文件内容到内存
        raw_data = f.read()
        
    # 检查文件头
    if raw_data[:8] != b'CTENFDAM':
        raise ValueError("这不是有效的NCM文件")
    
    offset = 10  # 初始偏移量
    
    # 读取密钥数据
    key_len = struct.unpack('<I', raw_data[offset:offset+4])[0]
    offset += 4
    key_data = bytearray(raw_data[offset:offset+key_len])
    offset += key_len
    
    # 对密钥数据进行异或操作
    for i in range(key_len):
        key_data[i] ^= 0x64
    # 使用AES解密密钥数据
    core_cipher = AES.new(CORE_KEY, AES.MODE_ECB)
    key_data = unpad(core_cipher.decrypt(bytes(key_data)), 16)
    key_data = key_data[17:]  # 去掉前17个字节
    
    # 生成密钥盒
    keybox = build_key_box(key_data)
    
    # 读取元数据
    meta_len = struct.unpack('<I', raw_data[offset:offset+4])[0]
    offset += 4
    meta_data = bytearray(raw_data[offset:offset+meta_len])
    offset += meta_len
    
    # 对元数据进行异或操作
    for i in range(meta_len):
        meta_data[i] ^= 0x63
        
    # 解码Base64并去除前22个字符
    meta_data = base64.b64decode(meta_data[22:])
    # 使用AES解密元数据
    meta_cipher = AES.new(META_KEY, AES.MODE_ECB)
    meta_data = unpad(meta_cipher.decrypt(meta_data), 16).decode('utf-8')
    
    # 解析元数据JSON
    meta_json = json.loads(meta_data[meta_data.index(':')+1:])
    
    # 根据TypeScript实现修正偏移量计算
    # 先计算新的offset值
    image_space = struct.unpack('<I', raw_data[offset+5:offset+9])[0]
    offset += image_space + 13
    
    # 提取音频数据并进行解密
    audio_data = bytearray(raw_data[offset:])
    audio_len = len(audio_data)
    print(f"音频数据长度: {audio_len}")
    
    # 使用密钥盒解密音频数据
    for i in range(audio_len):
        audio_data[i] ^= keybox[i & 0xff]
        
    return bytes(audio_data), meta_json

def build_key_box(key_data):
    """
    构建密钥盒 (TypeScript风格实现)
    
    Args:
        key_data (bytes): 密钥数据
        
    Returns:
        list: 密钥盒列表
    """
    # 创建0-255的数组
    box = list(range(256))
    key_data_len = len(key_data)
    
    # 第一次变换
    j = 0
    for i in range(256):
        j = (box[i] + j + key_data[i % key_data_len]) & 0xff
        box[i], box[j] = box[j], box[i]
    
    # 第二次变换
    key_box = []
    for i in range(256):
        idx = (i + 1) & 0xff
        si = box[idx]
        sj = box[(idx + si) & 0xff]
        key_box.append(box[(si + sj) & 0xff])
        
    return key_box

def save_audio_file(audio_data, meta, output_dir, target_format=None):
    """
    保存解密后的音频文件
    
    Args:
        audio_data (bytes): 解密后的音频数据
        meta (dict): 元数据字典
        output_dir (str): 输出目录
        target_format (str): 目标格式 ('mp3', 'flac')，如果为None则自动检测
    """
    # 获取音频格式
    if target_format:
        # 如果指定了目标格式，则使用指定格式
        audio_format = target_format.lower()
    else:
        # 否则根据音频数据推断格式
        audio_format = meta.get('format', '')
        if not audio_format:
            audio_format = sniff_audio_format(audio_data)
            
        if not audio_format:
            # 尝试从音频数据推断格式
            if len(audio_data) >= 3 and list(audio_data[:3]) == MP3_HEADER:  # MP3的ID3标签
                audio_format = 'mp3'
            elif len(audio_data) >= 4 and list(audio_data[:4]) == FLAC_HEADER:  # FLAC签名
                audio_format = 'flac'
            else:
                audio_format = 'mp3'  # 默认为MP3
            
    # 创建输出目录
    os.makedirs(output_dir, exist_ok=True)
    
    # 构造文件名
    title = meta.get('musicName', 'Unknown')
    artist = ""
    if meta.get('artist'):
        artists = [str(a[0]) for a in meta['artist'] if isinstance(a, (list, tuple))]
        artist = "_".join(artists)
        
    filename = f"{title}-{artist}.{audio_format}".replace("/", "_").replace("\\", "_")
    filepath = os.path.join(output_dir, filename)
    
    # 保存音频文件
    with open(filepath, 'wb') as f:
        f.write(audio_data)
        
    print(f"已保存: {filepath}")
    return filepath

def save_as_both_formats(audio_data, meta, output_dir):
    """
    将解密后的音频同时保存为MP3和FLAC格式
    
    Args:
        audio_data (bytes): 解密后的音频数据
        meta (dict): 元数据字典
        output_dir (str): 输出目录
    """
    print("正在保存为MP3格式...")
    mp3_path = save_audio_file(audio_data, meta, output_dir, 'mp3')
    
    print("正在保存为FLAC格式...")
    flac_path = save_audio_file(audio_data, meta, output_dir, 'flac')
    
    print(f"[OK] MP3文件已保存: {mp3_path}")
    print(f"[OK] FLAC文件已保存: {flac_path}")
    return mp3_path, flac_path

if __name__ == "__main__":
    import sys
    import argparse
    
    parser = argparse.ArgumentParser(description='网易云音乐NCM格式解密算法')
    parser.add_argument('ncm_file', help='NCM格式文件路径')
    parser.add_argument('--format', choices=['auto', 'mp3', 'flac', 'both'], 
                        default='auto', help='输出格式: auto(自动检测), mp3, flac, both(同时保存为mp3和flac)')
    parser.add_argument('--output', default='./decrypted', help='输出目录')
    
    args = parser.parse_args()
    
    ncm_file = args.ncm_file
    output_format = args.format
    output_dir = args.output
    
    if not os.path.exists(ncm_file):
        print(f"错误: 文件 '{ncm_file}' 不存在")
        sys.exit(1)
        
    try:
        print(f"正在解密文件: {ncm_file}")
        audio_data, meta = decrypt_ncm(ncm_file)
        print("解密成功!")
        print(f"歌曲名: {meta.get('musicName', 'Unknown')}")
        if meta.get('artist'):
            artists = [str(a[0]) for a in meta['artist'] if isinstance(a, (list, tuple))]
            print(f"艺术家: {', '.join(artists)}")
        print(f"专辑: {meta.get('album', 'Unknown')}")
        print(f"格式: {meta.get('format', 'Unknown')}")
        print(f"解密后音频数据长度: {len(audio_data)}")
        
        # 根据指定格式保存音频文件
        if output_format == 'both':
            print("正在同时保存为MP3和FLAC格式...")
            save_as_both_formats(audio_data, meta, output_dir)
        else:
            format_name = None if output_format == 'auto' else output_format
            saved_path = save_audio_file(audio_data, meta, output_dir, format_name)
            print(f"文件已保存至: {saved_path}")
        
    except Exception as e:
        print(f"解密失败: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)